﻿#define SHORT_RAYS
using System;
using RayDen.Library.Components.SystemComponents;
using RayDen.Library.Core;
using RayDen.Library.Core.Components;
using RayDen.Library.Core.Primitives;
using RayDen.RayEngine.Core.Interface;
using RayDen.RayEngine.Core.Types;

namespace RayDen.RayEngine.Data
{
    public sealed class TriangleLight : ILight, IGeoLightsource
    {
        bool computeNormal = false;
        bool normalInverse = false;
        private string meshName;

        public bool IsDelta
        {
            get
            {
                return false;
            }
        }

        public string Name
        {
            get { return meshName + triangleIndex.ToString(); }
        }

        internal float area;
        public Normal TriangleNormal;
        public int triangleIndex;
        [NonSerialized]
        public RayEngineScene scene;

        public ITriangleMesh Owner;
        public RgbSpectrumInfo Gain;
        public SampledSpectrum spectra;
        private static ISpectrum ZeroCSpectrumArray = SampledSpectrum.ZeroSpectrum();
        private static ISpectrum RgbSpectrumZeroArray = RgbSpectrum.ZeroSpectrum();
        private ISpectrum lightSpectra;
        public EmissionProfile Profile { get; set; }

        public TriangleLight(IRayEngineScene sc, string meshName, RgbSpectrum gain = new RgbSpectrum())
        {
            this.scene = (RayEngineScene)sc;
            this.meshName = meshName;
            this.Gain = (RgbSpectrumInfo)gain;
            //cie.stdillum.F1.spd
            //@"F:\3D\spds\cie.stdillum.F1.spd"
            //cie.stdillum.A.spd
            //macbeth-1.spd
            //FromFile(@"F:\3D\spds\cie.stdillum.F9.spd")
            spectra = SPD_Data.FromSampled(SPD_Data.FromFile(@"F:\3D\spds\cie.stdillum.D5000.spd"), SpectrumType.Illuminant);
        }

        public TriangleLight(RayEngineScene sc, int triIndex, ITriangleMesh mesh, RgbSpectrum gain = new RgbSpectrum())
            : this(sc, string.Empty, gain)
        {
            Gain = (RgbSpectrumInfo)gain;

            triangleIndex = triIndex;

            this.Owner = mesh;


            //Configuration.Instance.Get("Integrator.ComputeNormals", false);
            //Configuration.Instance.Get("Integrator.InverseLightNormals", false);

            var multiplicator = normalInverse ? -1f : 1f;
            this.TriangleNormal = multiplicator * ((computeNormal || !mesh.HasNormals) ? scene.Triangles[triangleIndex].ComputeNormal(scene.Vertices)
                : (Normal)scene.SceneGeometry.Normals[Math.Min(scene.SceneGeometry.Normals.Length - 1, triIndex)]);
            this.area = Math.Max(scene.Triangles[triangleIndex].AreaV(scene.Vertices), 0.00001f);
            Assert.IsNotNaN(this.TriangleNormal.x);
            Assert.IsTrue(this.area > 0f);
            lightSpectra = GlobalConfiguration.Instance.SpectralRendering ? (ISpectrum)spectra : gain;

        }

        #region IMaterial Members

        public bool IsLightSource()
        {
            return true;
        }
        public bool IsDiffuse()
        {
            return false;
        }
        public bool IsSpecular()
        {
            return false;
        }

        #endregion

        public RgbSpectrumInfo Emittance(ref Vector dir, out float pdf)
        {
            pdf = 1f / this.area;
            Normal n1 = this.TriangleNormal;
            if ((double)Normal.Dot(ref n1, ref dir) <= 0.0)
                return new RgbSpectrumInfo(0.0f);
            else
                return this.Gain;
        }
        public ISpectrum Le(ref Vector wo)
        {
            var sampleN = TriangleNormal;

            if (Normal.Dot(ref sampleN, ref wo) <= 0f)
            {
                return GlobalConfiguration.Instance.SpectralRendering ? ZeroCSpectrumArray : RgbSpectrumZeroArray;
            }

            return GlobalConfiguration.Instance.SpectralRendering ? (ISpectrum)spectra : Gain; // Light sources are supposed to have flat color
        }

        public void Le(ref Vector wo, out float pdf, ref RgbSpectrum spd)
        {
            pdf = 1f / area;

            if (Normal.Dot(ref TriangleNormal, ref wo) <= 0f)
            {
                spd.Mul(0f);
                return;
            }
            spd.Mul(ref lightSpectra);
        }

        public void SampleSpatialDirect(ref Point point, ref Normal n, float u0, float u1, float u2, ref LightSample ls)
        {
            int tries = 0;

#if VERBOSE
            try
            {
#endif
            var samplePoint = new Point();
            float b0, b1, b2;
            int maxTries = 1;
        startTry:

            TriangleDataInfo.Sample(ref scene.Triangles[triangleIndex], scene.Vertices, u2, u1, out samplePoint, out b0, out b1, out b2);
            var triangleNormal = TriangleNormal;
            //tri.AreaV(scene.Vertices);
            //var sampleN = TriangleNormal;
            //var N = n;

            ls.U = b0;
            ls.V = b1;
            Vector wi = samplePoint - point;
            //wi.Normalize();
            float distanceSquared = wi.Length2();
            ls.Distance = MathLab.Sqrt(distanceSquared);
            wi /= ls.Distance;

            var nw = -wi;
            float sampleNdotMinusWi = Normal.Dot(ref triangleNormal, ref nw);
            float NdotWi = Normal.Dot(ref n, ref wi);
            if ((sampleNdotMinusWi <= 0f) || (NdotWi <= 0f))
            {
                tries++;
                if (tries > maxTries)
                {
                    ls.Pdf = 0f;
                    return;
                }

                goto startTry;
            }

            ls.Pdf =
                //((1f / area) * distanceSquared / sampleNdotMinusWi) * (1f / mesh.TrianglesCount) * (1f / Math.Max(1, tries + 1));
                (distanceSquared / (sampleNdotMinusWi * area)) * (1f / Math.Max(1, tries + 1));




            // Using 0.01 instead of 0.0 to cut down fireflies
            if (ls.Pdf <= 0.01f)
            {
                ls.Pdf = 0f;
                return;
            }
            ls.LightRay.Dir = wi;
            //, MathLab.RAY_EPSILON, distance - MathLab.RAY_EPSILON);
#if VERBOSE
            }
            catch (Exception ex)
            {
                Tracer.TraceLine(ex.Message + ex.StackTrace);
                //Tracer.TraceLine("Triangle data offset "+ (mesh.StartTriangle).ToString() + "of " + triangleSampleData.Length);
                throw ex;
            }
#endif
        }


        public void SampleSpatialAdjoint(IRayEngineScene scen, float u0, float u1, float u2, float u3, float u4,
            out Vector dir, out float apdf, out float pdf)
        {
            var scene = (RayEngineScene)scen;
            float b0, b1, b2;
            Point orig;
            int index;

            var tri = scene.Triangles[triangleIndex];

            tri.Sample(scene.Vertices, u0, u1, out orig, out b0, out b1, out b2);
            // Ray direction
            var sampleN = -TriangleNormal;
            dir = MC.UniformSampleSphere(u2, u3);
            float rdotN = Normal.Dot(ref dir, ref sampleN);
            if (rdotN < 0f)
            {
                dir *= -1f;
                rdotN = -rdotN;
            }
            apdf = rdotN;

            pdf = ((MathLab.INVTWOPI / area));

        }

        public void EvaluateShadow(ref Point point, ref Normal normal, float u0, float u1, float u2, ref LightSample result)
        {
            if (result == null)
                result = new LightSample();

            var samplePoint = new Point();
            float b0, b1, b2;

            scene.Triangles[triangleIndex].Sample(scene.Vertices, u0, u1, out samplePoint, out b0, out b1, out b2);

            var sampleN =
                //-scene.Triangles[triangleIndex].ComputeNormal(scene.Vertices);
                //scene.Triangles[triangleIndex].InterpolateNormal(scene.Vertices, b0, b1);
            TriangleNormal.Normalize();
            var N = normal.Normalize();

            Vector wi = samplePoint - point;
            float distanceSquared = wi.Length2();
            float distance = MathLab.Sqrt(distanceSquared);
            wi /= distance;

            var nw = -wi;
            float sampleNdotMinusWi = Normal.Dot(ref sampleN, ref nw);
            float NdotWi = Normal.Dot(ref N, ref wi);
            if ((sampleNdotMinusWi <= 0f) || (NdotWi <= 0f))
            {
                result.Pdf = 0f;
                result.LightRay = new RayData();
                result.Spectrum = GlobalConfiguration.Instance.SpectralRendering ? ZeroCSpectrumArray : RgbSpectrumZeroArray;
                return;
            }


            result.LightRay = new RayData(ref point, ref wi,
#if SHORT_RAYS

 MathLab.RAY_EPSILON, distance - MathLab.RAY_EPSILON);
#else
                                         MathLab.RAY_EPSILON, distanceSquared);

#endif

            result.Pdf = (distanceSquared / (sampleNdotMinusWi * area));

            // Using 0.1 instead of 0.0 to cut down fireflies
            if (result.Pdf <= 0.1f)
            {
                result.Pdf = 0f;
                result.Spectrum = GlobalConfiguration.Instance.SpectralRendering ? ZeroCSpectrumArray : RgbSpectrumZeroArray;

                return;
            }
#if DEBUG
            Assert.IsNotNaN(result.Pdf);
#endif
            result.Distance = distance;
            result.Spectrum = Profile.Evaluate(b0, b1);
        }


        public void EvaluatePhoton(IRayEngineScene scen, float u0, float u1, float u2, float u3, float u4, out LightSample result)
        {
            result = new LightSample();
            var scene = (RayEngineScene)scen;
            float b0, b1, b2;
            Point orig;
            scene.Triangles[triangleIndex].Sample(scene.Vertices, u0, u1, out orig, out b0, out b1, out b2);

            // Ray direction
            var sampleN = this.TriangleNormal;
            Vector dir = MC.UniformSampleSphere(u2, u3);
            float RdotN = Normal.Dot(ref dir, ref sampleN);
            if (RdotN < 0f)
            {
                dir *= -1f;
                RdotN = -RdotN;
            }

            result.LightRay = new RayData(ref orig, ref dir);

            result.Pdf = (MathLab.INVTWOPI / area) * 1f / RdotN;

            result.Spectrum = this.lightSpectra;

        }

        public int SamplePoint(out Point samplePoint, float u0, float u1, out float pdf)
        {
            float b0, b1, b2;
            scene.Triangles[triangleIndex].Sample(scene.Vertices, u0, u1, out samplePoint, out b0, out b1, out b2);

            pdf = 1f / this.area;
            return triangleIndex;
        }

        public RgbSpectrumInfo Sample(ref Point p, ref Normal N, float u2, float u0, float u1, out RayData shadowRay, out float pdf)
        {
            Point samplePoint = new Point();
            float b0, b1, b2;

            scene.Triangles[triangleIndex].Sample(scene.Vertices, u0, u1, out samplePoint, out b0, out b1, out b2);
            //var sampleN = TriangleNormal;
            //var N = n;

            Vector wi = samplePoint - p;
            //wi.Normalize();
            float distanceSquared = wi.Length2();
            float distance = MathLab.Sqrt(distanceSquared);
            wi /= distance;

            var nw = -wi;
            float sampleNdotMinusWi = Normal.Dot(ref TriangleNormal, ref nw);
            float NdotWi = Normal.Dot(ref N, ref wi);
            if ((sampleNdotMinusWi <= 0f) || (NdotWi <= 0f))
            {
                pdf = 0f;
                shadowRay = new RayData();
                return new RgbSpectrumInfo(0f);
            }

            shadowRay = new RayData(ref p, ref wi, MathLab.RAY_EPSILON, distance - MathLab.RAY_EPSILON);
            pdf = (distanceSquared / (sampleNdotMinusWi * area));

            // Using 0.1 instead of 0.0 to cut down fireflies
            if (pdf <= 0.1f)
            {
                pdf = 0f;
                return new RgbSpectrumInfo(0);
            }
            return Gain;
        }


        #region IGeoLightsource Members

        public void Initialize(IRayEngineScene scen, params object[] data)
        {

            this.scene = (RayEngineScene)scen;
            var tri = scene.Triangles[triangleIndex];
            var computeNormal = true;
            //Configuration.Instance.Get("Integrator.ComputeNormals", false);
            var normalInverse = true;
            //Configuration.Instance.Get("Integrator.InverseLightNormals", false);
            var multiplicator = normalInverse ? -1f : 1f;
            this.TriangleNormal = multiplicator * (computeNormal ? tri.ComputeNormal(scene.Vertices) : (Normal)scene.SceneGeometry.Normals[triangleIndex]);
            this.area = tri.AreaV(scene.Vertices);
        }

        #endregion



        public void Le(ref Vector dir, ref ISpectrum le)
        {
            var sampleN = TriangleNormal;

            if (Normal.Dot(ref sampleN, ref dir) <= 0f)
            {
                le.Mul(0f);
                return;
            }

            var s = GlobalConfiguration.Instance.SpectralRendering ? (ISpectrum)spectra : Gain;
            le.Mul(ref s);
        }
    }
}